#include "consoleImpl.h"
#include "console.h"

using namespace std;

namespace {
	const int THE_DEFAULT_ATTR = FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED;
	const int THE_FOREGROUND_INTENSITY = FOREGROUND_INTENSITY;

	int ReverseAttribute(const int attribute) {
		return ((attribute & 0xf) << 4) | ((attribute & 0xf0) >> 4);
	}

	void SUCCEED_OR_DIE(BOOL x) {
		if (x) {
			return;
		}
		printf("Error: %d", GetLastError());
		exit(1);
	}

	void WriteEntry(const Entry& entry, CHAR_INFO* buffer, const int buffer_size, const bool highlight) {
		const int attribute = highlight ? entry.highlight_attr_ : entry.attr_;
		int pos = entry.pos_;
		for (size_t i = 0; i < entry.value_.size() && pos < buffer_size; ++i) {
			buffer[pos].Char.AsciiChar = entry.value_[i];
			buffer[pos].Attributes = attribute;
			++pos;
		}
	}
}

ConsoleImpl::ConsoleImpl(const string& title, const int width, const int height) : buffer_(NULL) {
	// Set up the handles for reading/writing:
	wHnd_ = GetStdHandle(STD_OUTPUT_HANDLE);
	rHnd_ = GetStdHandle(STD_INPUT_HANDLE);
	time_of_last_flush_ = timer_.GetTime();
	last_mouse_pos_ = { 0, 0 };
	last_mouse_buttons_state_ = 0;
	SetTitle(title);
	SetSize(width, height);
}

ConsoleImpl::~ConsoleImpl() {
	delete[] buffer_;
}

void ConsoleImpl::SetTitle(const string& title) {
	title_ = title;
	SetConsoleTitle(TEXT(title.c_str()));
}

void ConsoleImpl::SetSize(const int width, const int height) {
	width_ = width;
	height_ = height;
	SMALL_RECT windowSize = { 0, 0, width - 1, height - 1 };
	COORD bufferSize = { width, height };
	SUCCEED_OR_DIE(SetConsoleScreenBufferSize(wHnd_, bufferSize));
	SUCCEED_OR_DIE(SetConsoleWindowInfo(wHnd_, TRUE, &windowSize));
	delete[] buffer_;
	buffer_ = new CHAR_INFO[width*height];
	ClearBuffer();
}

float ConsoleImpl::TimeElapsedSinceLastFlush() const {
	return timer_.GetTime() - time_of_last_flush_;
}

int ConsoleImpl::GetEntryIndex(const int pos) const {
	if (pos < 0 || pos >= width_*height_ || entries_.empty()) {
		return Console::NO_ENTRY;
	}
	for (size_t i = entries_.size() - 1; i < entries_.size(); --i) {
		const auto& entry = entries_[i];
		if (pos >= entry.pos_&& pos < entry.pos_ + (int)entry.value_.size()) {
			return i;
		}
	}
	return Console::NO_ENTRY;
}

int ConsoleImpl::WriteString(
	const int row,
	const int col,
	const string& value,
	const int attr,
	const int highlight_attr) {
	if (row < 0 || row >= height_ || col < 0 || col >= width_) {
		return -1;
	}
	int pos = row*width_ + col;
	const int attribute = (attr == Console::DEFAULT_ATTRIBUTES ? THE_DEFAULT_ATTR : attr);
	int highlight_attribute = highlight_attr;
	switch (highlight_attribute) {
	case Console::NO_HIGHLIGHT:
		highlight_attribute = attribute;
		break;

	case Console::COLORS_HIGHLIGHT:
		highlight_attribute = attribute | THE_FOREGROUND_INTENSITY;
		break;

	case Console::REVERSE_COLORS_HIGHLIGHT:
		highlight_attribute = ReverseAttribute(attribute);
		break;
	}

	entries_.push_back(Entry(pos, value, attribute, highlight_attribute));
	return entries_.size() - 1;
}

void ConsoleImpl::ClearBuffer() {
	for (int row = 0; row < height_; ++row) {
		for (int col = 0; col < width_; ++col) {
			const int pos = row*width_ + col;
			buffer_[pos].Char.UnicodeChar = 0;
			buffer_[pos].Attributes = 0;
		}
	}
}

void ConsoleImpl::Flush() {
	const int mouse_pos = last_mouse_pos_.Y * width_ + last_mouse_pos_.X;

	// render
	ClearBuffer();
	for (const auto& e : entries_) {
		const bool highlight = (mouse_pos > e.pos_ && mouse_pos < e.pos_ + (int)e.value_.size());
		WriteEntry(e, buffer_, height_*width_, highlight);
	}

	// flush
	COORD charBufSize = { width_, height_ };
	COORD characterPos = { 0, 0 };
	SMALL_RECT writeArea = { 0, 0, width_ - 1, height_ - 1 };
	SUCCEED_OR_DIE(WriteConsoleOutput(wHnd_, buffer_, charBufSize, characterPos, &writeArea));

	// save time
	time_of_last_flush_ = timer_.GetTime();
}

void ConsoleImpl::ReadInputs(vector<Input>& inputs) {
	// clear the inputs
	inputs.clear();

	// read raw inputs from windows
	DWORD num_events = 0;
	SUCCEED_OR_DIE(GetNumberOfConsoleInputEvents(rHnd_, &num_events));
	if (num_events != 0)	{
		INPUT_RECORD* event_buffer = new INPUT_RECORD[num_events];
		DWORD num_events_read = 0;
		SUCCEED_OR_DIE(ReadConsoleInput(rHnd_, event_buffer, num_events, &num_events_read));
		for (DWORD i = 0; i < num_events_read; ++i)	{
			switch (event_buffer[i].EventType) {
			case MOUSE_EVENT:	{
				const DWORD button_state = event_buffer[i].Event.MouseEvent.dwButtonState;
				const DWORD filtered_button_state = button_state & ~last_mouse_buttons_state_;
				last_mouse_buttons_state_ = button_state;
				const DWORD L_Button = filtered_button_state & FROM_LEFT_1ST_BUTTON_PRESSED;
				const DWORD C_Button = filtered_button_state & FROM_LEFT_2ND_BUTTON_PRESSED;
				const DWORD R_Button = filtered_button_state & RIGHTMOST_BUTTON_PRESSED;
				const bool Button = L_Button || C_Button || R_Button;
				last_mouse_pos_ = event_buffer[i].Event.MouseEvent.dwMousePosition;
				const int pos = last_mouse_pos_.Y * width_ + last_mouse_pos_.X;
				if (Button)	{
					const int entry_index = GetEntryIndex(pos);
					Input input{ INPUT_MOUSE_LCLICK, entry_index, Position{ last_mouse_pos_.Y, last_mouse_pos_.X } };
					if (C_Button) {
						input.type_ = INPUT_MOUSE_CCLICK;
					}	else if (R_Button) {
						input.type_ = INPUT_MOUSE_RCLICK;
					}
					inputs.push_back(input);
				}
				break;
			}
			case KEY_EVENT:
				if (event_buffer[i].Event.KeyEvent.bKeyDown == TRUE) {
					inputs.push_back(Input{ INPUT_KEY_PRESSED, event_buffer[i].Event.KeyEvent.wVirtualKeyCode, Position{0,0}});
				}
				break;
			}
		}
		delete[] event_buffer;
	}

	// process them, fill the passed inputs vector

	// clear the entries
	entries_.clear();
}